﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using PropertyExpression.Common;

namespace PropertyExpression.ControllableQuery
{
    public struct Param<T>
    {
        internal readonly ParamContent Content;

        internal Param(ParamContent content)
        {
            Content = content;
        }
    }

    internal class ParamContent
    {
        internal readonly Func<string, SqlParameter> SqlParameterFunc;
        internal readonly Option<Action<SqlParameter>> SetValue;

        internal ParamContent(Func<string, SqlParameter> sqlParameterFunc, Option<Action<SqlParameter>> setValue)
        {
            SqlParameterFunc = sqlParameterFunc;
            SetValue = setValue;
        }
    }

    public static class Param
    {
        public static Param<Guid> New(Guid value)
        {
            return New(() => value);
        }

        public static Param<Guid> New(Func<Guid> value)
        {
            return New<Guid>(GuidInfo, new Option<Action<SqlParameter>>(_ => _.Value = value()));
        }

        public static Param<Guid> NewGuid()
        {
            return New<Guid>(GuidInfo);
        }

        private static Func<string, SqlParameter> GuidInfo
        {
            get { return name => new SqlParameter(name, SqlDbType.UniqueIdentifier); }
        }

        public static Param<Option<Guid>> New(Option<Guid> value)
        {
            return New(() => value);
        }

        public static Param<Option<Guid>> New(Func<Option<Guid>> value)
        {
            return New<Option<Guid>>(OptionalGuidInfo, new Option<Action<SqlParameter>>(_ => _.Value = value().GetValueOrDbNull()));
        }

        public static Param<Option<Guid>> NewOptionalGuid()
        {
            return New<Option<Guid>>(OptionalGuidInfo);
        }

        private static Func<string, SqlParameter> OptionalGuidInfo
        {
            get { return name => new SqlParameter(name, SqlDbType.UniqueIdentifier); }
        }

        public static Param<bool> New(bool value)
        {
            return New(() => value);
        }

        public static Param<bool> New(Func<bool> value)
        {
            return New<bool>(BoolInfo, new Option<Action<SqlParameter>>(_ => _.Value = value()));
        }

        public static Param<bool> NewBool()
        {
            return New<bool>(BoolInfo);
        }

        private static Func<string, SqlParameter> BoolInfo
        {
            get { return GetBooleanSqlParameterFunc(); }
        }

        public static Param<Option<bool>> New(Option<bool> value)
        {
            return New(() => value);
        }

        public static Param<Option<bool>> New(Func<Option<bool>> value)
        {
            return New<Option<bool>>(OptionalBool, new Option<Action<SqlParameter>>(_ => value().GetValueOrDbNull()));
        }

        public static Param<Option<bool>> NewOptionalBool()
        {
            return New<Option<bool>>(OptionalBool);
        }

        private static Func<string, SqlParameter> OptionalBool
        {
            get { return GetBooleanSqlParameterFunc(); }
        }

        public static Param<float> New(float value)
        {
            return New(() => value);
        }

        public static Param<float> New(Func<float> value)
        {
            return New<float>(FloatInfo, new Option<Action<SqlParameter>>(_ => _.Value = value()));
        }

        public static Param<float> NewFloat()
        {
            return New<float>(FloatInfo);
        }

        private static Func<string, SqlParameter> FloatInfo
        {
            get { return GetFloatSqlParameterFunc(); }
        }

        public static Param<Option<float>> New(Option<float> value)
        {
            return New(() => value);
        }

        public static Param<Option<float>> New(Func<Option<float>> value)
        {
            return New<Option<float>>(OptionalFloatInfo, new Option<Action<SqlParameter>>(_ => _.Value = value().GetValueOrDbNull()));
        }

        public static Param<Option<float>> NewOptionalFloat()
        {
            return New<Option<float>>(OptionalFloatInfo);
        }

        private static Func<string, SqlParameter> OptionalFloatInfo
        {
            get { return GetFloatSqlParameterFunc(); }
        }

        public static Param<string> New(string value)
        {
            return New(() => value);
        }

        public static Param<string> New(Func<string> value)
        {
            return New<string>(StringInfo, new Option<Action<SqlParameter>>(_ => _.Value = value() ?? (object) DBNull.Value));
        }

        public static Param<string> NewString()
        {
            return New<string>(StringInfo);
        }

        private static Func<string, SqlParameter> StringInfo
        {
            get { return GetStringSqlParameterFunc(); }
        }

        public static Param<DateTime> New(DateTime value)
        {
            return New(() => value);
        }
        public static Param<decimal> New(decimal value)
        {
            return New(() => value);
        }

        public static Param<DateTime> New(Func<DateTime> value)
        {
            return New<DateTime>(DateTimeInfo, new Option<Action<SqlParameter>>(_ => _.Value = value()));
        }

        public static Param<decimal> New(Func<decimal> value)
        {
            return New<decimal>(DecimalInfo, new Option<Action<SqlParameter>>(_ => {
                _.Value = value();
                var precision = _.Precision;
                _.Precision = 38;
                if (_.Scale < 7 && precision - _.Scale < 38 - 7)
                    _.Scale = 7;
            }));
        }

        public static Param<DateTime> NewDateTime()
        {
            return New<DateTime>(DateTimeInfo);
        }

        private static Func<string, SqlParameter> DateTimeInfo
        {
            get { return name => new SqlParameter(name, SqlDbType.DateTime); }
        }

        private static Func<string, SqlParameter> DecimalInfo
        {
            get { return name => new SqlParameter(name, SqlDbType.Decimal); }
        }

        public static Param<Option<string>> New(Option<string> value)
        {
            return New(() => value);
        }

        public static Param<Option<string>> New(Func<Option<string>> value)
        {
            return New<Option<string>>(OptionalStringInfo, new Option<Action<SqlParameter>>(_ => _.Value = value().GetValueOrDbNull()));
        }

        public static Param<Option<string>> NewOptionalString()
        {
            return New<Option<string>>(OptionalStringInfo);
        }

        private static Func<string, SqlParameter> OptionalStringInfo
        {
            get { return GetStringSqlParameterFunc(); }
        }

        public static Param<int> New(int value)
        {
            return New(() => value);
        }

        public static Param<int> New(Func<int> value)
        {
            return New<int>(IntInfo, new Option<Action<SqlParameter>>(_ => _.Value = value()));
        }

        public static Param<int> NewInt()
        {
            return New<int>(IntInfo);
        }

        public static Param<decimal> NewDecimal()
        {
            return New<decimal>(DecimalInfo);
        }

        private static Func<string, SqlParameter> IntInfo
        {
            get { return name => new SqlParameter(name, SqlDbType.Int); }
        }

        public static Param<Option<int>> New(Option<int> value)
        {
            return New(() => value);
        }

        public static Param<Option<int>> New(Func<Option<int>> value)
        {
            return New<Option<int>>(OptionalIntInfo, new Option<Action<SqlParameter>>(_ => _.Value = value().GetValueOrDbNull()));
        }

        public static Param<Option<int>> NewOptionalInt()
        {
            return New<Option<int>>(OptionalIntInfo);
        }

        private static Func<string, SqlParameter> OptionalIntInfo
        {
            get { return name => new SqlParameter(name, SqlDbType.Int); }
        }

        public static Param<ITableValued<T>> New<T>(ITableValued<T> value)
        {
            return New(() => value);
        }

        public static Param<ITableValued<T>> New<T>(Func<ITableValued<T>> value)
        {
            return New<ITableValued<T>>(TableValuedInfo<T>(),
                new Option<Action<SqlParameter>>(_ => _.Value = ConvertToDataTable(value().Value)));
        }

        public static Param<ITableValued<T>> NewTableValued<T>()
        {
            return New<ITableValued<T>>(TableValuedInfo<T>());
        }

        private static Func<string, SqlParameter> TableValuedInfo<T>()
        {
            return parameterName => new SqlParameter(parameterName, SqlDbType.Structured) {
                TypeName = typeof (T).Name
            };
        }

        private static Func<string, SqlParameter> GetFloatSqlParameterFunc()
        {
            return name => new SqlParameter(name, SqlDbType.Float);
        }

        private static Func<string, SqlParameter> GetStringSqlParameterFunc()
        {
            return name => new SqlParameter(name, SqlDbType.NVarChar) { Size = -1 };
        }

        private static Func<string, SqlParameter> GetBooleanSqlParameterFunc()
        {
            return name => new SqlParameter(name, SqlDbType.Bit);
        }

        private static object GetValueOrDbNull<T>(this Option<T> value)
        {
            return value.Match(_ => _, () => (object)DBNull.Value);
        }

        private static DataTable ConvertToDataTable<T>(IEnumerable<T> value)
        {
            var dataTable = new DataTable();
            var propertyInfos = typeof(T).GetProperties();
            foreach (var propertyInfo in propertyInfos)
            {
                dataTable.Columns.Add(propertyInfo.Name, propertyInfo.PropertyType);
            }
            foreach (var item in value)
            {
                foreach (var propertyInfo in propertyInfos)
                {
                    var dataRow = dataTable.NewRow();
                    dataRow[propertyInfo.Name] = propertyInfo.GetValue(item, null);
                    dataTable.Rows.Add(dataRow);
                }
            }
            return dataTable;
        }

        private static Param<T> New<T>(Func<string, SqlParameter> func)
        {
            return new Param<T>(new ParamContent(func, new Option<Action<SqlParameter>>()));
        }

        private static Param<T> New<T>(Func<string, SqlParameter> func,
            Option<Action<SqlParameter>> setValue)
        {
            return new Param<T>(new ParamContent(func, setValue));
        }
    }

    public static class ParamExtensions
    {
        public static Param<Guid> Param(this Guid value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<Option<Guid>> Param(this Option<Guid> value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<bool> Param(this bool value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<Option<bool>> Param(this Option<bool> value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<float> Param(this float value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<Option<float>> Param(this Option<float> value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<string> Param(this string value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<DateTime> Param(this DateTime value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<decimal> Param(this decimal value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<Option<string>> Param(this Option<string> value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<int> Param(this int value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<Option<int>> Param(this Option<int> value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<ITableValued<T>> Param<T>(this ITableValued<T> value)
        {
            return ControllableQuery.Param.New(value);
        }

        public static Param<ITableValued<T>> Param<T>(this IEnumerable<T> value)
        {
            return ControllableQuery.Param.New(value.AsTableValued());
        }
    }
}